import React, { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { ConfigProvider, message } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import { HashRouter } from 'react-router';
import { Provider } from 'react-redux';
import Panel from './panel';
import store from './store';
import { bindVariableChange } from '@/runner/vars'
import { askBg } from './ext/ipc';
import { parseBoolLike, flow, guardVoidPromise, until } from '@/common/ts_utils';
import * as CONSTANT from './common/constant';
import log from '@/common/log';
import * as act from './actions';
import FuzzySet from 'fuzzyset.js';
import { delay, randomName, dataURItoBlob, getPageDpi, downloadByBlob } from './common/utils';
import Ext, { isFirefox } from './common/web_extension';
import { activateTab } from './common/tab_utils';
import { askTab } from './ext/ipc';
import storage from './common/ext_storage_local';
import { getMacroExtraKeyValueData } from './services/kv_data/macro_extra_data';
import { RunBy } from './reducers';
import { getStorageManager, StorageManagerEvent } from '@/file_storage'
import { getMacroFileNodeList, findMacroNodeWithCaseInsensitiveRelativePath, findMacroFolderWithCaseInsensitiveRelativePath, getShouldLoadResources, editorCommandCount, getIndexToInsertRecorded } from './recomputed';
import { commandWithoutBaseUrl } from './models/test_case_model';
import { ocrLanguageOptions } from './services/ocr/languages';
import { player } from './runner/player';
import { renderLog } from './common/macro_log';
import { getDownloadMan } from './common/download_man';
import { fromJSONString, fromHtml } from './common/convert_utils';
import { PLAY_MODE, END_REASON } from '@/types/player'
import { CaptureScreenshotService } from '@/common/capture_screenshot'
import { captureVisibleTabAsync } from '@/ext/ext'
import { onTimeoutStatus } from '@/runner/timeout_counter'
import globalConfig from './config';
import "react-virtualized/styles.css";
import 'react-resizable/css/styles.css';
import './index.css'

bindVariableChange()

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <ConfigProvider locale={zhCN}>
      <HashRouter>
        <Provider store={store}>
          <Panel />
        </Provider>
      </HashRouter>
    </ConfigProvider>
  </StrictMode>,
)

const updatePageTitle = (args: any) => {
  // Note: Firefox includes page url in title, there could be not enough space for tab title
  if (isFirefox()) return true;
  const origTitle = document.title.replace(/ - .*$/, '');
  document.title = `${origTitle} - (Tab: ${args.title})`;
};

export function findSameNameTestSuite(name: string, testSuites: any[]) {
  const toLower = (str: string) => (str || '').toLowerCase()
  return testSuites.find((ts: any) => toLower(ts.name) === toLower(name))
}

const genOverrideScope = ({ options }: { options: any }) => {
  return Object.keys(options || {}).reduce((prev: any, key: any) => {
    const m = key.match(/^cmd_var(1|2|3)$/i);
    if (!m) return prev;

    prev[`!CMD_VAR${m[1]}`] = options[key];
    return prev;
  }, {});
};
const validParams: string[] = ['direct', 'closeBrowser', 'closeKantu', 'closeRPA', 'continueInLastUsedTab', 'nodisplay', 'folder', 'savelog', 'storage', 'macro', 'testsuite', 'storageMode', 'loadmacrotree', 'cmd_var1', 'cmd_var2', 'cmd_var3', 'cmd_var4', 'cmd_var5', 'cmd_var6', 'cmd_var7', 'cmd_var8', 'cmd_var9', 'cmd_var10'];
const fuzzyObj = FuzzySet(validParams);

const guardCommandLineArgs = (args: any) => {
  // Check params
  const keys = Object.keys(args);
  const checkName = (pattern: any, str: any) => {
    if (typeof pattern === 'string') {
      return pattern === str;
    } else {
      return pattern.test(str);
    }
  };
  const checkValue = (name: any, value: any) => {
    switch (name) {
      case 'continueInLastUsedTab':
      case 'closeKantu':
      case 'closeRPA':
      case 'closeBrowser':
      case 'direct':
      case 'loadmacrotree':
      case 'nodisplay':
        if (/^0|1|true|false$/i.test(value)) {
          return true;
        } else {
          throw new Error(`"${name}" should be 0, 1, true or false, but now it's ${value}`);
        }

      case 'storage':
        if (['browser', 'xfile'].indexOf(value) !== -1) {
          return true;
        } else {
          throw new Error(`"${name}" should be either browser or xfile, but now it's ${value}`);
        }

      default:
        return true;
    }
  };

  keys.forEach((key) => {
    if (key.trim().length === 0) {
      return;
    }

    const isValid = validParams.find((name) => checkName(name, key));

    if (!isValid) {
      const match = fuzzyObj.get(key);
      const guess = !match || !match[0] || !match[0][1] ? '' : `, do you mean "${match[0][1]}"?`;
      store.dispatch(act.addLog('warning', `Unknown command line parameter: "${key}"${guess}`));
    }

    try {
      checkValue(key, args[key]);
    } catch (e: any) {
      store.dispatch(act.addLog('warning', `Invalid value for cmd line arg: ${e.message}`));
    }
  });
};

const initFromCommandLineArgs = (args: any) => {
  const loadMacroTree = parseBoolLike(args.loadmacrotree);
  const noDisplay = parseBoolLike(args.nodisplay, false);

  if (loadMacroTree) {
    store.dispatch(act.setFrom(RunBy.Manual));
  }

  if (noDisplay) {
    store.dispatch(act.setNoDisplayInPlay(true));
  }
};

const prepareBeforeRun = (options: any) => {
  if (options.savelog) {
    store.dispatch(act.clearLogs());
  }
};

const downloadTextFile = (text: string, fileName: string) => {
  const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
  downloadByBlob(blob, fileName);
};

const genPlayerPlayCallback = ({ options, installed }: { options: any, installed: boolean }) => {
  // Only run this callback once, we've added it to two places
  // 1. Player callback
  // 2. Promise finally of the entire macro run
  let alreadyRun = false;
  return (err: any, reason: any) => {
    if (alreadyRun) {
      return;
    }

    alreadyRun = true;

    let pSaveLog = delay(() => { }, 1000);

    if (options.savelog) {
      const isFullPath = /\\|\//.test(options.savelog);

      const logs = store.getState().logs;
      const errorLog = logs.find((log) => log.type === 'error' && !(log.options && log.options.ignored));
      const error = err || (errorLog && { message: errorLog.text });
      const logTitle = error ? `Status=Error: ${error.message}` : `Status=OK`;
      const logContent = logs.map((log) => renderLog(log, false));
      const text = [logTitle, '###', ...logContent].join('\n');

      if (isFullPath) {


        if (installed && installed != undefined) {

          return getDownloadMan().prepareDownload(options.savelog);

        } else {
          pSaveLog = delay(() => { }, 500).then(() => {
            downloadTextFile(text, decodeURIComponent(options.savelog));
            // Note: We have to wait until savelog download completes if there is any
            return getDownloadMan().prepareDownload(options.savelog);
          });
        }
      } else {
        if (!isFullPath) {
          pSaveLog = delay(() => { }, 500).then(() => {
            downloadTextFile(text, decodeURIComponent(options.savelog));
            // Note: We have to wait until savelog download completes if there is any
            return getDownloadMan().prepareDownload(options.savelog);
          });
        } else {
          pSaveLog = () => { }
        }
      }
    }

    const closeBrowser = parseBoolLike(options.closeBrowser, false);
    const closeRPA = parseBoolLike(options.closeRPA !== undefined ? options.closeRPA : options.closeKantu, true);

    if (closeBrowser && reason !== END_REASON.MANUAL) {
      // Close all tabs If close option is set
      pSaveLog
        .catch((e: any) => {
          log.warn('Save log error: ', e.message);
        })
        .then(() => askBg('PANEL_CLOSE_ALL_WINDOWS', {}));
    }
    // 最小化IDE,而不是关闭IDE
    const minimizedRpa = true;
    // 关闭打开的playTab
    const closePlayTab = true;
    if (closePlayTab) {
      askBg('PANEL_CLOSE_ALL_WINDOWS_BUT_INVOKE', {});
    }
    // Note: it's better to keep kantu open if it's opened manually before
    // 关闭IDE
    if (!err && reason === END_REASON.COMPLETE && closeRPA && !closeBrowser) {
      // Close kantu panel
      setTimeout(() => {
        window.close();
      }, 1000);
    } else if (!err && reason === END_REASON.COMPLETE && minimizedRpa && !closeRPA && !closeBrowser) {
      // 最小化ide
      setTimeout(() => {
        askBg('PANEL_MINIMIZE', {});
      }, 1000);
    }
  };
};

const bindMacroAndTestSuites = () => {

  const macroStorage = getStorageManager().getMacroStorage();

  const restoreTestCases = () => {
    store.dispatch(act.setIsLoadingMacros(true));

    const pMacrosExtra = getMacroExtraKeyValueData()
      .getAll()
      .then((data: any) => {
        // log('restoreMacrosExtra', data)

        store.dispatch(act.setMacrosExtra(data));
      });

    const pFolderStructure = (() => {
      if (!getShouldLoadResources(store.getState())) {
        return Promise.resolve();
      }

      return macroStorage.listR().then((entryNodes: any) => {
        // log('restoreMacroFolderStructure', entryNodes)

        store.dispatch(act.setMacroFolderStructure(entryNodes));
      });
    })();

    return Promise.all([pMacrosExtra, pFolderStructure]).finally(() => store.dispatch(act.setIsLoadingMacros(false)));
  };


  return flow(guardVoidPromise(restoreTestCases));
};

// Note: editing is stored in localstorage
const restoreEditing = () => {
  return storage.get('editing').then((editing) => {
    if (!editing) return;

    let finalEditing = editing;

    if (editing.baseUrl) {
      finalEditing = { ...editing };
      finalEditing.commands = finalEditing.commands.map(commandWithoutBaseUrl(editing.baseUrl));
      delete finalEditing.baseUrl;
    }

    store.dispatch(act.setEditing(finalEditing));
  });
};

// preset #210
// uncomment the following line to activate it
// DefaultStorageMode =  StorageStrategyType.XFile

const restoreConfig = () => {
  return storage.get('config').then((config) => {
    const cfg = {
      showSidePanel: false,
      useDarkTheme: false,
      sidePanelOnLeft: false,
      anthropicAPIKey: '',
      aiComputerUseMaxLoops: 50,
      useInitialPromptInAiChat: true,
      aiChatSidebarPrompt: 'Describe what you see, in 10 words or less.',
      showSettingsOnStart: false,
      showSidebar: false,
      showBottomArea: true,
      playScrollElementsIntoView: true,
      playHighlightElements: true,
      playCommandInterval: 0.3,
      // selenium related
      saveAlternativeLocators: true,
      recordNotification: true,
      recordClickType: 'click',
      showTestCaseTab: true,
      logFilter: 'All',
      onErrorInLoop: 'continue_next_loop',
      // Run macros from outside
      allowRunFromBookmark: true,
      allowRunFromFileSchema: true,
      allowRunFromHttpSchema: true,
      // timeout in seconds
      timeoutPageLoad: 60,
      timeoutElement: 10,
      timeoutMacro: 0,
      timeoutDownload: 60,
      // backup relative
      lastBackupActionTime: new Date().getTime(),
      enableAutoBackup: true,
      autoBackupInterval: 7,
      autoBackupTestCases: true,
      autoBackupTestSuites: true,
      autoBackupScreenshots: true,
      autoBackupCSVFiles: true,
      autoBackupVisionImages: true,
      // security relative
      shouldEncryptPassword: 'no',
      masterPassword: '',
      // variable relative
      showCommonInternalVariables: true,
      showAdvancedInternalVariables: false,
      // xmodules related
      storageMode: 'Browser',
      xmodulesStatus: 'unregistered',
      // orc related
      ocrCalibration: 6,
      ocrCalibration_internal: 6,
      ocrScaling: 100,
      ocrEngine: 98,
      ocrMode: 'enabled', // 'disabled',
      ocrLanguage: 'eng',
      ocrLanguageOption: ocrLanguageOptions,
      ocrOfflineURL: '',
      ocrOfflineAPIKey: '',
      // vision related
      cvScope: 'browser',
      defaultVisionSearchConfidence: 0.6,
      useDesktopScreenCapture: true,
      waitBeforeDesktopScreenCapture: false,
      secondsBeforeDesktopScreenCapture: 3,
      // proxy related,
      defaultProxy: '',
      defaultProxyAuth: '',
      turnOffProxyAfterReplay: true,
      ...config,
    };
    store.dispatch(act.updateConfig(cfg));
    return cfg;
  });
};

const restoreCSV = () => {
  if (!getShouldLoadResources(store.getState())) {
    return Promise.resolve();
  }

  // Note: just try to init storage. Eg. For browser fs, it will try to create root folder
  getStorageManager().getCSVStorage();
  return store.dispatch(act.listCSV());
};

const restoreScreenshots = () => {
  getStorageManager().getScreenshotStorage();
  return store.dispatch(act.listScreenshots());
};

const restoreVisions = () => {
  if (!getShouldLoadResources(store.getState())) {
    return Promise.resolve();
  }

  getStorageManager().getVisionStorage();
  return store.dispatch(act.listVisions());
};

const reloadResources: any = (): Promise<any> => {
  const p = bindMacroAndTestSuites().then(() => {
    return flow(
      guardVoidPromise(restoreCSV),
      guardVoidPromise(restoreVisions),
      guardVoidPromise(restoreScreenshots),
      guardVoidPromise(() => store.dispatch(act.resetEditingIfNeeded()))
    );
  });

  reloadResources.onLastReloadFinished = (callback: any) => (callback ? p.then(callback) : p);
  return p;
}

const bindIpcEvent = () => {
  const prepareByOptions = (options: any = {}) => {
    const lowerCaseOptions = Object.keys(options).reduce((prev: any, key: any) => {
      prev[key.toLowerCase()] = options[key];
      return prev;
    }, {});

    if (parseBoolLike(lowerCaseOptions.continueinlastusedtab, false)) {
      return askBg('PANEL_CLOSE_CURRENT_TAB_AND_SWITCH_TO_LAST_PLAYED', {});
    } else {
      return Promise.resolve();
    }
  };

  const handleCommand = (cmd: any, args: any): any => {
    // log(cmd, args)

    switch (cmd) {
      case 'PROXY_UPDATE': {
        // 更新代理
        store.dispatch(act.updateProxy(args.proxy));
        return true;
      }

      case 'OPEN_SETTINGS':
        // 打开设置
        store.dispatch(act.updateUI({ showSettings: true }));
        return true;

      case 'INSPECT_RESULT':
        // 捕获元素
        store.dispatch(act.doneInspecting());
        store.dispatch(
          act.updateSelectedCommand({
            target: args.locatorInfo.target,
            targetOptions: args.locatorInfo.targetOptions,
          })
        );
        return true;

      case 'RECORD_ADD_COMMAND':
        // 录制添加命令
        log('got add command', cmd, args);
        args.id = window.crypto.randomUUID();
        const state = store.getState();
        const commandCount = editorCommandCount(state);
        const recordIndex = getIndexToInsertRecorded(state);
        const shouldSkip = state.recorder.skipOpen && args.cmd === 'open';

        store.dispatch(act.toggleRecorderSkipOpen(false));

        if (shouldSkip) {
          return false;
        }

        if (recordIndex > 0 && recordIndex <= commandCount) {
          store.dispatch(act.insertCommand(args, recordIndex, true));
        } else {
          store.dispatch(act.appendCommand(args, true));
        }

        return true;
      case 'TIMEOUT_STATUS':
        // 设置超时状态
        if (store.getState().status !== CONSTANT.APP_STATUS.PLAYER) {
          return;
        }
        if (args.playUID && !player.checkPlayUID(args.playUID)) {
          return;
        }

        store.dispatch(act.setTimeoutStatus(args));
        return true;
      case 'RUN_TEST_CASE': {
        // 运行测试用例
        if (store.getState().status !== CONSTANT.APP_STATUS.NORMAL) {
          message.error('can only run macros when it is not recording or playing');
          return false;
        }

        const { testCase, options } = args;

        guardCommandLineArgs(options);
        initFromCommandLineArgs(options);

        const storageMan = getStorageManager();

        Promise.resolve()
          .then(() => prepareByOptions(options))
          .then(() => {
            const state = store.getState();
            const shouldLoadResources = getShouldLoadResources(state);

            if (!shouldLoadResources) {
              return Promise.resolve(true);
            }

            return new Promise((resolve) => {
              resolve(reloadResources.onLastReloadFinished ? reloadResources.onLastReloadFinished() : null);
            }).then(() =>
              until(
                'macros ready',
                () => {
                  const state = store.getState();
                  const macroNodes = getMacroFileNodeList(state);

                  return {
                    pass: macroNodes && macroNodes.length > 0,
                    result: true,
                  };
                },
                1000,
                20 * 1000
              )
            );
          })
          .then(() => {
            // Note: for backward compatibility, still use `name` field (which makes sense in flat fs mode) to store `path`
            // after we migrate to standard folder mode
            const state = store.getState();
            const shouldLoadResources = getShouldLoadResources(state);
            let macroPath = testCase.name;

            if (shouldLoadResources) {
              const found = findMacroNodeWithCaseInsensitiveRelativePath(state, testCase.name);

              if (!found) {
                throw new Error(`Can't find macro with name "${testCase.name}"`);
              }

              macroPath = found.fullPath;
            }

            const errorMsg = `No macro found with path '${macroPath}'`;

            return storageMan
              .getMacroStorage()
              .read(macroPath, 'Text')
              .then(
                (macro: any) => {
                  if (!macro) {
                    message.error(errorMsg);
                    throw new Error(errorMsg);
                  }

                  return macro;
                },
                (e: any) => {
                  if (/File size cannot be determined.|A requested file or directory could not be found/.test(e.message)) {
                    throw new Error(errorMsg);
                  } else {
                    return Promise.reject(e);
                  }
                }
              )
              .then((tc: any) => {

                const openTc = tc.data.commands.find((item: any) => item.cmd.toLowerCase() === 'open');

                prepareBeforeRun(options);

                const callback = genPlayerPlayCallback({ options, installed: false, });

                store.dispatch(act.editTestCase(tc.id));
                store
                  .dispatch(
                    act.playerPlay({
                      macroId: tc && tc.id,
                      title: macroPath,
                      extra: {
                        id: tc && tc.id,
                      },
                      mode: PLAY_MODE.STRAIGHT,
                      startIndex: 0,
                      startUrl: openTc ? openTc.target : null,
                      resources: tc.data.commands,
                      postDelay: state.player.playInterval * 1000,
                      overrideScope: genOverrideScope({ options }),
                      callback: callback,
                    })
                  )
                  .finally(callback);

                store.dispatch(act.updateUI({ sidebarTab: 'Macro' }));
              });
          })
          .catch((e) => {
            store.dispatch(act.addLog('error', e.message));
          });

        return true;
      }

      // 运行测试套件
      case 'RUN_TEST_SUITE': {
        if (store.getState().status !== CONSTANT.APP_STATUS.NORMAL) {
          message.error('can only run test suites when it is not recording or playing');
          return false;
        }

        const { testSuite, options } = args;

        guardCommandLineArgs(options);
        initFromCommandLineArgs(options);


        const storageMan = getStorageManager();

        Promise.resolve()
          .then(() => prepareByOptions(options))
          .then(() => {
            const state = store.getState();
            const shouldLoadResources = getShouldLoadResources(state);

            if (testSuite.macroFolder && testSuite.macroFolder.length > 0) {
              const pMacroNodes = (() => {
                if (shouldLoadResources) {
                  return until(
                    'macros ready',
                    () => {
                      const state = store.getState();
                      const macroNodes = getMacroFileNodeList(state);

                      return {
                        pass: macroNodes && macroNodes.length > 0,
                        result: macroNodes,
                      };
                    },
                    1000,
                    20 * 1000
                  ).then(() => {
                    const folder = findMacroFolderWithCaseInsensitiveRelativePath(store.getState(), testSuite.macroFolder);
                    return (folder && folder.children) || [];
                  });
                }

                return storageMan
                  .getMacroStorage()
                  .listR(testSuite.macroFolder)
                  .then((nodes) => nodes.filter((node) => node.isFile));
              })();

              return pMacroNodes.then((foundNodes) => {
                const macroStorage = storageMan.getMacroStorage();
                const dirPath = macroStorage.dirPath(testSuite.macroFolder.replace(/\\/g, '/'));
                const path = macroStorage.getPathLib();
                const folderName = path.basename(dirPath);

                if (foundNodes.length === 0) {
                  throw new Error(`No folder found for ${testSuite.macroFolder}, or no macro found in it`);
                }

                prepareBeforeRun(options);

                player.play({
                  title: folderName,
                  mode: PLAY_MODE.STRAIGHT,
                  startIndex: 0,
                  resources: foundNodes.map((item: any) => ({
                    id: item.fullPath,
                    loops: 1,
                  })),
                  extra: {
                    id: dirPath,
                    name: folderName,
                  },
                  public: {
                    scope: genOverrideScope({ options }),
                  },
                  callback: genPlayerPlayCallback({ options, installed: false }),
                });
              });
            }

            if (testSuite.name && testSuite.name.length > 0) {
              const pTestSuite = (() => {
                if (shouldLoadResources) {
                  return until('testSuites ready', () => {
                    const state = store.getState();
                    const { testSuites } = state.editor;

                    return {
                      pass: testSuites && testSuites.length > 0,
                      result: true,
                    };
                  }).then(() => {
                    const state = store.getState();
                    return findSameNameTestSuite(testSuite.name, state.editor.testSuites);
                  });
                }

                return storageMan.getTestSuiteStorage().read(testSuite.name, 'Text');
              })();

              return pTestSuite.then((ts) => {
                if (!ts) {
                  message.error(`no macro found with name '${testSuite.name}'`);
                  return false;
                }

                prepareBeforeRun(options);

                player.play({
                  title: ts.name,
                  extra: {
                    id: ts.id,
                    name: ts.name,
                  },
                  mode: PLAY_MODE.STRAIGHT,
                  startIndex: 0,
                  resources: ts.cases.map((item: any) => ({
                    id: item.testCaseId,
                    loops: item.loops,
                  })),
                  public: {
                    scope: genOverrideScope({ options }),
                  },
                  callback: genPlayerPlayCallback({ options, installed: false }),
                });

                return store.dispatch(act.updateUI({ sidebarTab: 'test_suites' }));
              });
            }
          })
          .catch((e) => {
            store.dispatch(act.addLog('error', e.message));
          });

        return true;
      }

      case 'IMPORT_AND_RUN': {
        // 导入并运行
        const { options } = args;
        let testCase;

        if (args.html) {
          try {
            testCase = fromHtml(args.html);
          } catch (e) {
            message.error('Failed to parse html', 1.5);
            return false;
          }
        }

        if (args.json) {
          try {
            const jsonStr = typeof args.json === 'string' ? args.json : JSON.stringify(args.json);
            testCase = fromJSONString(jsonStr);
          } catch (e) {
            message.error('Failed to parse json', 1.5);
            return false;
          }
        }

        if (!testCase) {
          message.error('Nothing to import');
          return false;
        }

        guardCommandLineArgs(options);

        const storageMode = 'Browser';
        const storageMan = getStorageManager();

        return Promise.resolve()
          .then(() => prepareByOptions(options))
          .then(() => {
            const state = store.getState();
            const shouldLoadResources = getShouldLoadResources(state);

            if (!shouldLoadResources) {
              return Promise.resolve(true);
            }

            return new Promise((resolve) => {
              resolve(reloadResources.onLastReloadFinished ? reloadResources.onLastReloadFinished() : null);
            }).then(() => {
              return until(
                'macros ready',
                () => {
                  const state = store.getState();
                  const macroNodes = getMacroFileNodeList(state);

                  return {
                    pass: macroNodes && macroNodes.length > 0,
                    result: true,
                  };
                },
                1000,
                20 * 1000
              );
            });
          })
          .then(() => {
            return store
              .dispatch(act.upsertTestCase(testCase))
              .then(() => store.dispatch(act.editTestCase(testCase.name)))
              .then((macro: any) => {
                const state = store.getState();
                const openTc = macro.data.commands.find((command: any) => command.cmd.toLowerCase() === 'open');

                store.dispatch(
                  act.playerPlay({
                    macroId: macro.id,
                    title: macro.name,
                    extra: {
                      id: macro.id,
                    },
                    mode: PLAY_MODE.STRAIGHT,
                    startIndex: 0,
                    startUrl: openTc ? openTc.target : null,
                    resources: macro.data.commands,
                    postDelay: state.player.playInterval * 1000,
                    overrideScope: genOverrideScope({ options }),
                    callback: genPlayerPlayCallback({ options, installed: false }),
                  })
                );
                return true;
              })
              .catch((e: any) => {
                log.error(e.stack);
                throw e;
              });
          });
      }

      case 'ADD_VISION_IMAGE': {
        // 添加视觉图片
        const { dataUrl, requireRename = false } = args;
        const fileName = `${randomName()}_dpi_${getPageDpi()}.png`;

        return getStorageManager()
          .getVisionStorage()
          .write(fileName, dataURItoBlob(dataUrl))
          .then(() => restoreVisions() as any)
          .then(() => {
            if (!requireRename) return { fileName };

            return (store.dispatch(act.renameVisionImage(fileName)) as any)
              .then((fileName: string) => {
                restoreVisions();
                return { fileName };
              });
          });
      }

      case 'RESTORE_SCREENSHOTS': {
        // 恢复截图
        restoreScreenshots();
        return true;
      }

      case 'UPDATE_ACTIVE_TAB': {
        // 更新活动标签
        updatePageTitle(args);
        return true;
      }

      case 'IS_ACTIVE': {
        // 是否活动
        return true;
      }

      case 'ADD_LOG': {
        // 添加日志
        if (!args) return false;
        if (args.info) store.dispatch(act.addLog('info', args.info, args.options));
        if (args.warning) store.dispatch(act.addLog('warning', args.warning));
        if (args.error) store.dispatch(act.addLog('error', args.error));

        return true;
      }

      case 'SCREEN_AREA_SELECTED': {
        // 屏幕区域选择
        const captureScreenshotService = new CaptureScreenshotService({
          captureVisibleTab: () => {
            return captureVisibleTabAsync(args.windowId, { format: 'png' }) as Promise<any>
          }
        });
        return captureScreenshotService
          .captureScreenInSelectionSimple(args.tabId, {
            rect: args.rect,
            devicePixelRatio: args.devicePixelRatio,
          })
          .then((dataUrl: string | Blob) => {
            return handleCommand('ADD_VISION_IMAGE', { dataUrl, requireRename: false });
          });
      }

      case 'STORE_SCREENSHOT_IN_SELECTION': {
        // 存储屏幕截图
        const { tabId, windowId, rect, devicePixelRatio, fileName } = args;

        return Promise.resolve()
          .then(() => {
            return activateTab(tabId, true)
              .then(() => delay(() => { }, CONSTANT.SCREENSHOT_DELAY))
              .then(() => {
                const captureScreenshotService = new CaptureScreenshotService({
                  captureVisibleTab: () => {
                    return captureVisibleTabAsync(windowId, { format: 'png' }) as Promise<any>
                  }
                });
                return captureScreenshotService.captureScreenInSelection(
                  tabId,
                  { rect, devicePixelRatio },
                  {
                    startCapture: () => {
                      return askTab(tabId, 'START_CAPTURE_FULL_SCREENSHOT', { hideScrollbar: false }).then((res: any) => {
                        return res.result;
                      });
                    },
                    endCapture: (pageInfo: any) => {
                      return askTab(tabId, 'END_CAPTURE_FULL_SCREENSHOT', { pageInfo }).then((res: any) => {
                        return res.result;
                      });
                    },
                    scrollPage: (offset: any) => {
                      return askTab(tabId, 'SCROLL_PAGE', { offset }).then((res: any) => {
                        return res.result;
                      });
                    },
                  },
                  {}
                )
              })
              .then((dataUrl: string | Blob) => {
                return getStorageManager()
                  .getScreenshotStorage()
                  .overwrite(fileName, dataURItoBlob(dataUrl as string))
                  .then(() => {
                    handleCommand('RESTORE_SCREENSHOTS', {});
                    return fileName;
                  });
              });
          });
      }
    }
  };

  Ext.runtime.onMessage.addListener((req: any, sender: any, sendResponse: any) => {
    if (req.type === 'BG_ASK_PANEL') {
      Promise.resolve().then(() => handleCommand(req.cmd, req.args)).then(res => {
        sendResponse(res)
      }).catch((e: any) => {
        console.error('BG_ASK_PANEL Error ==:>> ', e)
        sendResponse({
          data: null,
          success: false,
          error: e.message
        })
      })
    }
    return true
  })
};

const bindWindowEvents = () => {
  // reset status to normal when panel closed
  window.addEventListener('beforeunload', () => {
    askBg('PANEL_STOP_RECORDING', {});
    askBg('PANEL_STOP_PLAYING', {});
  });

  window.addEventListener('resize', () => {

    const size = {
      width: window.outerWidth,
      height: window.outerHeight,
    };
    const state = store.getState();
    store.dispatch(
      act.updateConfig({
        size: {
          ...state.config.size,
          [state.config.showSidebar ? 'with_sidebar' : 'standard']: size,
        },
      })
    );
  });

  window.addEventListener('message', (e) => {
    switch (e.data && e.data.type) {
      case 'RELOAD_VISIONS':
        return store.dispatch(act.listVisions());
    }
  });

  onTimeoutStatus((payload: any) => {
    if (store.getState().status !== CONSTANT.APP_STATUS.PLAYER) {
      return;
    }
    if (payload.playUID && !player.checkPlayUID(payload.playUID)) {
      return;
    }

    store.dispatch(act.setTimeoutStatus(payload));
  });
};

function initProxyState() {
  askBg('PANEL_GET_PROXY').then((proxy: any) => {
    store.dispatch(act.updateProxy(proxy));
  });
}

function tryPreinstall() {
  return storage.get('preinstall_info').then((info) => {
    const status = (() => {
      if (!info) return 'fresh';

      const { askedVersions = [] } = info;
      if (askedVersions.indexOf(globalConfig.preinstall.version) === -1) {
        return 'new_version_available';
      }

      return 'up_to_date';
    })();

    switch (status) {
      case 'fresh':
        return store.dispatch(act.preinstall());

      case 'new_version_available':
        return store.dispatch(act.updateUI({ newPreinstallVersion: true }));

      case 'up_to_date':
      default:
        return false;
    }
  });
}

function bindStorageModeChanged() {
  let first = true;

  getStorageManager().on(StorageManagerEvent.StrategyTypeChanged, (type) => {
    if (first) {
      first = false;
      return;
    }

    try {
      Promise.resolve().then(reloadResources).then(() => {
        // store.dispatch(act.selectInitialMacro(type));
      });
    } catch (e) {
      log.warn(e);
    }
  });

  getStorageManager().on(StorageManagerEvent.RootDirChanged, () => {
    reloadResources();
  });

  getStorageManager().on(StorageManagerEvent.ForceReload, () => {
    reloadResources();
  });
}

function init() {

  // initFromQuery();

  bindIpcEvent();

  bindWindowEvents()

  bindVariableChange();
  bindStorageModeChanged();
  restoreEditing();
  restoreConfig();

  initProxyState();

  tryPreinstall()
    .catch((e) => {
      log.warn('Error in preinstall', e);
    })
    .then(() => {
      reloadResources();
    });

  askBg('I_AM_PANEL', {});

  document.title = document.title + ' ' + Ext.runtime.getManifest().version;
  askBg('PANEL_CURRENT_PLAY_TAB_INFO', {}).then(updatePageTitle);

}

init();